/*
Rilego - An easy Images 2 E-Book converter.
Copyright (C) 2011  Federico Improta (Kirys)

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */
package it.kirys.rilego.engine;

import it.kirys.rilego.engine.loaders.imagesources.IImageSource;
import it.kirys.rilego.engine.outputbuilders.BuilderException;
import it.kirys.rilego.engine.outputbuilders.IOutputBuilder;
import it.kirys.rilego.engine.processors.IImageProcessor;
import it.kirys.rilego.engine.project.Project;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This class is the main engine.
 * @author Federico Improta
 */
public class Engine {

	public static final String LIB_NAME = "RilegoLib";
	public static final String LIB_VERSION = "dev";
	List<ICallBack> callbacks;
	OutputStream outputStream;
	Project project;
	long startTime;
	int concurentWorkersNumber;

	class MainEngineProcess implements Runnable {

		Engine engine;
		IOutputBuilder builder;
		Project project;

		@Override
		public void run() {
			int i = 0;
			int size = engine.getImageProcess().size();
			engine.sendInfo("Working..", new EngineStatusInfo(0, EngineStatusInfo.Status.Running));
			for (Future<IImageProcessor> f : engine.getImageProcess()) {
				int p = (i * 100) / size;
				engine.sendInfo("Working..", new EngineStatusInfo(p, EngineStatusInfo.Status.Running));
				while (!f.isDone()) {
					try {
						Thread.sleep(500);
					} catch (InterruptedException ex) {
					}
				}
				if (f.isCancelled()) {
					//TODO: the operation has been cancelled, so we have to terminate!
				} else {
					try {
						// f has done we have to procede
						IImageProcessor processor = null;
						processor = f.get();
						try {
							engine.logMessage(String.format("Adding %s",processor.getSourceImage().getIdentifier()));
							builder.add2Output(processor.getOutput());
						} catch (Exception ex) {
							logException("The builder could not save an image:", ex);
						}
					} catch (InterruptedException ex) {
						logException("Interrupted", ex);
					} catch (ExecutionException ex) {
						logException("One Worker could not complete its process due to:", ex.getCause());
					}
				}
				i++;
			}
			engine.sendInfo("Finishing..", new EngineStatusInfo(100, EngineStatusInfo.Status.Running));
			engine.end(false);
		}//public void run()

		public MainEngineProcess(Engine engine, Project project) {
			this.engine = engine;
			this.project = project;
			this.builder = project.getBuilder();
		}
	}//class MainEngineProcess implements Runnable
	ExecutorService monitorExecutor;
	ExecutorService workersExecutor;
	Future<MainEngineProcess> monitor = null;
	ArrayList<Future<IImageProcessor>> workers;

	/**
	 * Send a message to all callbacks
	 * @param message
	 */
	synchronized public void logMessage(String message) {
		for (ICallBack c : callbacks) {
			c.logMessage(message);
		}
	}//public void logMessage(String message)

	/**
	 * Long an exception
	 * @param message
	 * @param ex
	 */
	synchronized public void logException(String message, Throwable ex) {
		StringWriter w = new StringWriter();
		PrintWriter p = new PrintWriter(w);
		ex.printStackTrace(p);
		p.flush();
		for (ICallBack c : callbacks) {
			c.logMessage(String.format("%s\n %s", message, w.toString()));
		}
	}//public void logMessage(String message)

	/**
	 * Send info to all callbacks
	 * @param message
	 * @param info
	 */
	synchronized public void sendInfo(String message, EngineStatusInfo info) {
		for (ICallBack c : callbacks) {
			if (info != null) {
				c.sendInfo(message, new EngineStatusInfo(info));
			} else {
				c.sendInfo(message, null);
			}
		}
	}//public void sendInfo (String message, EngineStatusInfo info)

	/**
	 * Add a callback to the callback list
	 * if the reference is already present the callback will not be added
	 * @param cb callback to add
	 */
	public void addCallBack(ICallBack cb) {
		for (ICallBack c : callbacks) {
			if (c == cb) {
				return;
			}
		}
		callbacks.add(cb);
	}//public void addCallBack(ICallBack cb)

	/**
	 * Create the processor based on the source
	 * @param source source to create the processor for
	 * @return the generate processor
	 */
	public IImageProcessor createProcessor(IImageSource source) {
		return project.getFactory().generateProcessor(source, project.getBuilder(), this);
	}//private IImageProcessor createProcessor(IImageSource source)

	private void startWorkers() {
		for (IImageSource s : project.getSources()) {
			IImageProcessor p = createProcessor(s);
			Future<IImageProcessor> f = workersExecutor.submit(p, p);
			workers.add(f);
		}
	}//private void startWorkers()

	public boolean isWorking() {
		if ((monitorExecutor != null) && (workersExecutor != null)) {
			return true;
		} else {
			return false;
		}
	}//public boolean isWorking()

	/**
	 * Get start time
	 * @return start time in milliseconds
	 */
	public long getStartTime() {
		return startTime;
	}//public long getStartTime()

	/**
	 * Get elapsed time since start was fired
	 * @return elapsed time in milliseconds
	 */
	public long getElapsed() {
		return java.lang.System.currentTimeMillis() - startTime;
	}//public long getElapsed()

	synchronized public void start() {
		startTime = java.lang.System.currentTimeMillis();
		if (!isWorking()) {
			if (project == null) {
				throw new NullPointerException("Project cannot be null");
			} else if (project.getFactory() == null) {
				throw new NullPointerException("Factory cannot be null");
			} else if (project.getBuilder() == null) {
				throw new NullPointerException("Output builder cannot be null");
			}
			monitorExecutor = Executors.newSingleThreadExecutor();
			if (concurentWorkersNumber>1){
				workersExecutor = Executors.newFixedThreadPool(concurentWorkersNumber);
			} else {
				workersExecutor = Executors.newSingleThreadExecutor();
			}

			workers.clear();
			try {
				project.getBuilder().open(outputStream, project);

			} catch (IOException ex) {
				//TODO: we have to handle the thing
			} catch (BuilderException ex) {
				//TODO: we have to handle the thing
			}
			MainEngineProcess monitorProcess = new MainEngineProcess(this, project);
			startWorkers();
			monitor = (Future<MainEngineProcess>) monitorExecutor.submit(monitorProcess);
		}
	}//synchronized public void start()

	public Project getProject() {
		return project;
	}//public Project getProject()

	public void setProject(Project project) {
		this.project = project;
	}//public void setProject(Project project)

	public void waitCompletition() {
		while (!monitor.isDone()) {
			try {
				Thread.sleep(1000);
			} catch (InterruptedException ex) {
				//TODO: Logger.getLogger(Engine.class.getName()).log(Level.SEVERE, null, ex);
			}
		}
	}//public void waitCompletition()

	/**
	 * Stop the engine Not implemented yet
	 *
	 */
	public void stop() {
		//TODO: implement the stop
	}//public void stop()

	protected void fail() {
		end(true);
	}//protected void fail()

	protected void cleanOut() {
		monitorExecutor = null;
		workersExecutor = null;
		workers.clear();
	}//protected void cleanOut()

	protected void end(boolean isFail) {
		try {
			project.getBuilder().close();
			cleanOut();
		} catch (Exception ex) {
			logException("Closing Error", ex);
			isFail = true;
		}
		sendInfo("Completed", new EngineStatusInfo(100, EngineStatusInfo.Status.Finished));
		for (ICallBack c : callbacks) {
			c.processEnded(isFail);
		}
	}//protected void end(boolean isFail)

	synchronized public ArrayList<Future<IImageProcessor>> getImageProcess() {
		return workers;
	}//synchronized public ArrayList<Future<IImageProcessor>> getImageProcess()

	public OutputStream getOutputStream() {
		return outputStream;
	}//public OutputStream getOutputStream()

	public void setOutputStream(OutputStream outputStream) {
		this.outputStream = outputStream;
	}//public void setOutputStream(OutputStream outputStream)

	public int getConcurentWorkersNumber() {
		return concurentWorkersNumber;
	}

	public void setConcurentWorkersNumber(int concurentWorkersNumber) {
		this.concurentWorkersNumber = validateConcurentWorkersNumber(concurentWorkersNumber);
	}

	/**
	 * Return a valid number of concurrent workers
	 * if the number is less than 1 1 will be returned
	 * if the number is more that 10 10 will be returned
	 * @param s source value
	 * @return valid value
	 */
	static int validateConcurentWorkersNumber(int s){
		if (s<1){
			return 1;
		} else if (s>10){
			return 10;
		} else {
			return s;
		}
	}//int validateConcurentWorkersNumber(int s)

	public Engine() {
		this(1);
	}//public Engine()

	public Engine(int concurentWorkersNumber) {
		workers = new ArrayList<Future<IImageProcessor>>();
		callbacks = new LinkedList<ICallBack>();
		this.concurentWorkersNumber = validateConcurentWorkersNumber(concurentWorkersNumber);
	}//public Engine(int concurentWorkersNumber)

}
